package ferp.android.views.table;

import android.content.Context;
import android.graphics.*;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.widget.LinearLayout;
import ferp.android.GUI;
import ferp.android.R;
import ferp.android.managers.ProfileManager;
import ferp.android.views.CardView;
import ferp.android.views.table.animation.MovingCard;
import ferp.android.views.table.controller.CardDoubleTapController;
import ferp.android.views.table.controller.CardDragController;
import ferp.android.views.table.controller.Controller;
import ferp.android.views.table.controller.DropController;
import ferp.android.views.table.element.bid.BidElement;
import ferp.android.views.table.element.bid.BidLeft;
import ferp.android.views.table.element.bid.BidRight;
import ferp.android.views.table.element.button.Offer;
import ferp.android.views.table.element.button.UndoDrop;
import ferp.android.views.table.element.image.Mark;
import ferp.android.views.table.element.image.SandClock;
import ferp.android.views.table.element.image.Turn;
import ferp.android.views.table.element.misere.MisereDesk;
import ferp.android.views.table.element.text.FirstHand;
import ferp.android.views.table.element.text.Watermark;
import ferp.android.views.table.element.trick.TrickCounter;
import ferp.android.views.table.element.trick.TrickElement;
import ferp.android.views.table.element.tutorial.TutorialNote;
import ferp.android.widgets.buttons.BidButton;
import ferp.android.widgets.buttons.BidShifter;
import ferp.android.widgets.buttons.TextButton;
import ferp.android.widgets.buttons.TwoLayerButton;
import ferp.android.widgets.buttons.rank.RankButton;
import ferp.android.widgets.buttons.suit.SuitButton;
import ferp.core.card.Card;
import ferp.core.game.Bid;
import ferp.core.game.Game;
import ferp.core.game.Settings;
import ferp.core.game.Trick;
import ferp.core.log.Log;
import ferp.core.mode.Mode;
import ferp.core.player.Hand;
import ferp.core.player.Player;
import ferp.core.tutorial.Scenario;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;

/**
 * User: igorgok
 * Date: 9/8/11 10:30 AM
 */
public class TableView extends ViewGroup implements CardView.Listener
{
  public interface Listener
  {
    void onTouch();

    void onOfferButtonClicked();
    void onInputButtonClicked(View view, int id);
    void onUndoDropButtonClicked(Game g);

    void onCardReleased(Card c);
    void onCardCommit(Card c);

    void onTreeReady(Player mover) throws Game.Error;
  }

  public TableView(Context context, AttributeSet as)
  {
    super(context, as);

    // create views
    views = new Views(context);

    setOnTouchListener(new OnTouchListener()
    {
      public boolean onTouch(View view, MotionEvent motionEvent)
      {
        Log.debug(Log.TAG, "onTouch: " + motionEvent.getAction());

        controller.onTouch(view, motionEvent);

        return false;
      }
    });

    setWillNotDraw(false);
  }

  // all internal components, like MovingCard, should use these method and NOT receive game and settings as parameters,
  // because current profile is reloaded and becomes a completely new object in 'onStop' event when Settings activity is opened
  public Game game() {return pm.getCurrentProfile().game();}
  public Settings settings() {return pm.getCurrentProfile().settings;}

  public void initialize(Listener listener, ProfileManager pm, boolean square)
  {
    this.pm = pm;
    this.listener = listener;
    this.square = square;

    // initialize views
    views.initialize();

    // initialize moving card wrapper
    mc = new MovingCard(listener, this, views.trick.view());

    // initialize controllers
    controller = default_ = new Controller(listener, this);
    drop = new DropController(listener, this, views.input.drop, views.input.without3);
    drag = new CardDragController(listener, this, mc, areas.sink);
    doubleTap = new CardDoubleTapController(listener, this, mc, areas.sink);
  }

  public void reset(Game g, Settings settings, boolean hide)
  {
    // advance current controller
    advanceController(g, settings);

    // hide player bids
    hideBids(g);

    // set player cards
    for (Player p : g.players())
    {
      // set new cards for the player and disable them
      showPlayerCards(g, settings, p.id());
      enablePlayerCards(p.id(), false);

      // hide bid views
      //views.bids[p.id()].setVisibility(INVISIBLE);
    }

    // show talon cards face down
    if (hide)
      hideTalon();
    else
      setTalon(g.talon());

    // gone trick cards
    hideTrick(g, false);

    // reset trick counters
    for (TrickCounter tc : views.trickCounter)
    {
      tc.show(!hide);
      tc.reset();
    }

    views.misere.reset();
    views.firstHand.hide();
    views.sandClock.hide();
    views.turn.hide();
    views.offer.hide();
    views.watermark.show(areas.watermark, g);

    for (Mark mark : views.mark)
      mark.hide();

    for (CardView cv : views.drop)
      cv.setVisibility(INVISIBLE);

    views.note.hide();
  }

  public void removeAnimations()
  {
    for (int i = 0; i < Game.PLAYERS; ++i)
    {
      List<CardView> hand = views.hands.get(i);

      for (CardView cv : hand)
      {
        Animation a = cv.getAnimation();

        if (a != null)
        {
          a.setAnimationListener(null);
          a.cancel();
        }

        cv.clearAnimation();
      }
    }
  }

  public void resetController() {controller.reset();}
  public void resetControllerState() {controller.resetState();}

  public void advanceController(Game g, Settings settings)
  {
    Controller previous = controller;

    // reset previous controller to move back card view that has been elevated/shifted from player's hand
    previous.reset();

    // advance to next controller
    if (g.mode() == Mode.dropping && Player.isHuman(g.declarer()))
      controller = drop;
    else
      if (Mode.isMoving(g.mode()))
        switch (settings.cardMovingMode)
        {
          case DRAG:
            controller = drag;
            break;

          case DOUBLE_TAP:
            controller = doubleTap;
            break;
        }
      else
        controller = default_;

    if (controller != previous)
    {
      // reset next controller to easily resolve various scenarios related to game restoring when table view is already exist, i.e. pausing/resuming activity
      controller.reset();

      Log.debug(Log.TAG, "switching TableView controller: " + previous.getClass().getSimpleName() + " -> " + controller.getClass().getSimpleName());
    }
  }

  public void layoutMark(int player, CardView cv, int i) {views.mark[i].layout(areas.mark[i], player, cv);}
  public void showMark(int player, CardView cv, int i) {views.mark[i].show(areas.mark[i], player, cv);}
  public void hideMark(int i) {views.mark[i].hide();}

  public void showBidPlay(Bid bid)
  {
    views.input.bid.show(bid);
    views.input.bid.setVisibility(View.VISIBLE);
  }

  public void showBidMisere() {views.input.misere.setVisibility(View.VISIBLE);}

  public void showBidPass() {showBidPass(0);}

  public void showBidPass(int factor)
  {
    String pass = getResources().getString(R.string.input_pass);

    views.input.pass.set(factor == 0 ? pass : GUI.getColoredText(pass + " ", "red", "×" + factor));
    views.input.pass.setVisibility(View.VISIBLE);
  }

  public void showDrop()
  {
    views.input.drop.setEnabled(drop.isDropEnabled());
    views.input.drop.setVisibility(View.VISIBLE);
  }

  public void showWithout3()
  {
    views.input.without3.setEnabled(drop.isWithout3Enabled());
    views.input.without3.setVisibility(View.VISIBLE);
  }

  public void showContractPlay(Game g, Bid bid)
  {
    views.undoDrop.show(areas.undoDrop[0]);
    views.input.show4WayDeclareControl(g, bid);
  }

  public void showWhistFull() {views.input.whistFull.setVisibility(View.VISIBLE);}
  public void showWhistHalf() {views.input.whistHalf.setVisibility(View.VISIBLE);}
  public void showWhistPass() {views.input.whistPass.setVisibility(View.VISIBLE);}

  public void showOpenStand()
  {
    Game g = game();

    if (Trick.size(g.trick.current) == 0)
    {
      views.input.stand.setVisibility(View.VISIBLE);
      views.input.layDown.setVisibility(View.VISIBLE);
    }
    else
    {
      views.input.stand2.setVisibility(View.VISIBLE);
      views.input.layDown2.setVisibility(View.VISIBLE);
    }
  }

  public void showTricking10Confirm()
  {
    views.input.agree10.setVisibility(View.VISIBLE);
    views.input.play10.setVisibility(View.VISIBLE);
  }

  public void showMisereConfirm()
  {
    views.undoDrop.show(areas.undoDrop[1]);
    views.input.miserePlay.setVisibility(View.VISIBLE);
  }

  public void showOffer() {views.offer.show(areas.offer);}

  public void markBidPlay() {showMark(views.input.bid);}
  public void markBidMisere() {showMark(views.input.misere);}
  public void markBidPass() {showMark(views.input.pass);}
  public void markDrop() {showMark(views.input.drop);}
  public void markWhistFull() {showMark(views.input.whistFull);}
  public void markWhistHalf() {showMark(views.input.whistHalf);}
  public void markWhistPass() {showMark(views.input.whistPass);}
  public void markMiserePlay() {showMark(views.input.miserePlay);}

  public void markStand()
  {
    Game g = game();

    if (Trick.size(g.trick.current) == 0)
      showMark(views.input.stand);
    else
      showMark(views.input.stand2);
  }

  public void markLaydown()
  {
    Game g = game();

    if (Trick.size(g.trick.current) == 0)
      showMark(views.input.layDown);
    else
      showMark(views.input.layDown2);
  }

  public void markCard(int player, Card c, int i)
  {
    CardView cv = views.find(player, c);

    // cv could be null if the card is in a trick when restoring the game
    if (cv != null)
      views.mark[i].show(areas.mark[i], player, cv);
  }

  public void showMisereDesk(Game g, Settings settings, int cards)
  {
    views.misere.set(cards, settings.rankOrder);
    views.misere.show(areas.misere[g.player.declarer], areas.bid[0], areas.trick, areas.turn[0], areas.hand[2], areas.trickCounter[0], settings.isLandscape(), PADDING, g.player.declarer);
  }

  public void markMisereDesk(Card c) {views.misere.mark(c);}

  public void hideOptions()
  {
    for (View v : views.input.elements)
      v.setVisibility(View.GONE);

    views.undoDrop.hide();
    views.offer.hide();

    if (game().tutorial())
      views.note.hide();
  }

  // show cards in player's hand
  public void showPlayerCards(Game g, Settings settings, Player p) {showPlayerCards(g, settings, p.id());}
  public void showPlayerCards(Game g, Settings settings, int pid) {showPlayerCards(settings, pid, g.getHand(pid).current, g.getHand(pid).suits, g.getHand(pid).open);}

  // show given cards for specific player
  public void showPlayerCards(Settings settings, int player, int hash, ArrayList<Card.Suit> suits, boolean show)
  {
    Hand.getOrderedSuits(hash, settings.suitOrder, suits);
    List<CardView> cvs = views.hands.get(player);
    List<Card> cards = Hand.cards(hash, suits, settings.rankOrder, show);
    int i = 0;

    for (; i < cards.size(); i++)
    {
      cvs.get(i).setVisibility(VISIBLE);
      cvs.get(i).set(cards.get(i), show);
      // to show players cards uncomment the following string
      //cvs.get(i).set(cards.get(i), show || BuildConfig.DEBUG);
    }

    // hide all the rest
    for (; i < cvs.size(); ++i)
      cvs.get(i).setVisibility(GONE);

    // redraw player hand
    layoutPlayerCards(settings, player);
  }

  public void enablePlayerCards(int id, boolean enable)
  {
    List<CardView> hv = views.hands.get(id);

    for (CardView cv : hv)
      cv.setEnabled(enable);
  }

  public void enablePlayerCards(int id, int hand)
  {
    List<CardView> hv = views.hands.get(id);

    for (CardView cv : hv)
      if (cv.getVisibility() == VISIBLE)
        cv.setEnabled(Hand.contains(hand, cv.card().hash));
  }

  public void layoutPlayerCards(Settings settings, int id)
  {
    if (Player.isHuman(id))
      layoutHumanCards(settings);
    else
      layoutAICards(settings, id);
  }

  // move dropped cards back to human's player in case of invalid drop in tutorial mode
  public void resetDrop() {controller.reset();}

  public int getDrop()
  {
    CardView[] selected = drop.selected();

    return selected[0].card().hash | selected[1].card().hash;
  }

  public void showInitialPosition(Game g, Settings settings)
  {
    // hide trick, turn and misere desk views because set can be finished in the middle of a round
    views.trick.hide();
    views.turn.hide();
    views.misere.hide();
    views.watermark.hide();

    // show talon cards
    openTalon();

    boolean drop = false;

    for (int i = 0; i < Game.PLAYERS; ++i)
    {
      int hand;

      // in case of passing or 'without 3' drop cards shouldn't be shown
      if (i == g.player.declarer && g.drop()[0] != 0 && g.drop()[1] != 0)
      {
        hand = Hand.remove(Hand.add(g.getDeclarerHand().initial, g.talon()), g.drop());
        drop = true;
      }
      else
        hand = g.getHand(i).initial;

      showBid(g.player(i), g.getBid(i), false);
      showPlayerCards(settings, i, hand, g.getHand(i).suits, true);
    }

    // show replay indicator and first hand
    showFirstHand(g.getFirstHandPlayer());

    // show dropped cards
    if (drop)
    {
      layoutDropCards(g);
      showDropCards(g);
    }
  }

  public void moveCardToTrick(int player, Card card, boolean android)
  {
    // "gone" the card from the player's hand
    CardView cv = views.find(player, card);

    // for android mover initialize the card to animate
    // in case of human mover it's already done
    if (android)
      mc.initialize(cv, null);

    // move the card
    mc.commit(player);
  }

  public void undoMove(int player)
  {
    CardView source = views.trick.view().getTargetCardView(player);

    mc.initialize(source);
    mc.undo(views.find(player, source.card()));
  }

  public void hideTrick(Game g, Settings settings)
  {
    // if playing passing and not Rostov and round < 2 - hide played talon card as well
    hideTrick(g, g.bid == null && !settings.isRostov() && g.rounds < 2);
  }

  public void showTrick(int trick)
  {
    // show players cards by they turn
    for (int i = 0; i < Game.PLAYERS; ++i)
    {
      Card card = Trick.card(trick, i);

      if (card != null)
        views.trick.show(Trick.owner(trick, i), card);
    }
  }

  public void showTrickCard(int player, Card card) {views.trick.show(player, card);}

  public void setTalon(int[] talon)
  {
    int i = 0;

    for (CardView cv : views.talon)
    {
      cv.set(Card.card(talon[i++]), false);
      cv.setVisibility(VISIBLE);
    }
  }

  public void openTalonCard(int i)
  {
    // face up talon card
    CardView talon = views.talon.get(i);

    talon.show(true);
    talon.setVisibility(VISIBLE);
    talon.bringToFront();
  }

  public void openTalon()
  {
    for (int i = 0; i < Game.TALON_SIZE; ++i)
      openTalonCard(i);
  }

  public void closeTalonCard(int i)
  {
    // face down talon card
    CardView talon = views.talon.get(i);

    talon.show(false);
    talon.setVisibility(VISIBLE);
  }

  public void hideTalonCard(int i)
  {
    views.talon.get(i).setVisibility(INVISIBLE);
  }

  public void hideTalon()
  {
    for (int i = 0; i < Game.TALON_SIZE; ++i)
      hideTalonCard(i);
  }

  public void showBid(Player p, Game.BidData bid, boolean animation)
  {
    // update the "model"
    bid.visible = true;
    // animate only for androids and not resumed game
    views.bids[p.id()].show(bid.current, !Player.isHuman(p) && animation);
  }

  public void hideBids(Game g)
  {
    for (Player p : g.players())
    {
      // update the "model"
      g.getBid(p.id()).visible = false;
      // hide the callout
      views.bids[p.id()].hide();
    }
  }
  
  public void updateTrickCount(Game g, Player p) {views.trickCounter[p.id()].update(g.getTricks(p.id()));}
  public void setTrickCount(int player, int tricks) {views.trickCounter[player].set(tricks);}

  public void showFirstHand(int first)
  {
    views.firstHand.show(first == 1 ? R.drawable.first_hand_left : R.drawable.first_hand_right, areas.firstHand[first]);
  }

  public void hideFirstHand() {views.firstHand.hide();}

  public void showClock(Game g, int id) {views.sandClock.show(areas.clock[id]);}
  public void hideClock() {views.sandClock.hide();}

  public void showTurn(int id) {views.turn.show(areas.turn[id]);}
  public void hideTurn() {views.turn.hide();}

  public void onMovingStarted(CardView cv, Point touch) {controller.onMovingStarted(game().player.current, cv, touch);}
  public void onMoving(CardView cv, int x, int y) {controller.onMoving(cv, x, y);}
  public void onMovingEnded(CardView cv) {controller.onMovingEnded(cv);}

  public void applySettings()
  {
    Game g = game();
    Settings settings = settings();

    // advance controller as card moving mode might have changed
    advanceController(g, settings);

    // refresh player cards (card order might have changed)
    for (Player p : g.players())
      showPlayerCards(g, settings, p);

    // switch rank order on misere desk as it might have changed
    if (g.bid != null && g.bid.isMisere() && g.declarer() != g.human())
    {
      views.misere.switchOrder(settings.rankOrder);
      // this it to resolve problem of show/hide rank layouts
      layoutMisereDesk(g, settings.isLandscape());
    }
  }

  public void onOfferDialogDismissed() {controller.resetState();}

  public void showNote(Scenario.Note note)
  {
    views.note.show(areas.note[note.position().ordinal()], note.text(pm.getCurrentProfile().settings.locale));


    //views.note.show(note.text(profile.settings.locale), areas.note[note.position().ordinal()]);

/*
    Rect target = areas.note[note.position().ordinal()];

    if (views.note.area() != target)
    {
      int dx = target.left - views.note.area().left;
      int dy = target.top - views.note.area().top;
      TranslateAnimation ta = new TranslateAnimation(0, dx, 0, dy);

      ta.setInterpolator(new DecelerateInterpolator());
      ta.setDuration(5000);
      ta.setFillAfter(true);
      ta.setZAdjustment(Animation.ZORDER_NORMAL);

      views.note.view().startAnimation(ta);
    }
*/
  }

  @Override
  protected void onDraw(Canvas canvas)
  {
    super.onDraw(canvas);

    areas.draw(canvas);
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b)
  {
    Game g = game();
    Settings settings = pm.getCurrentProfile().settings;

    if (g != null)
    {
      int width = r - l;
      int height = b - t;

      if (changed)
      {
        // initialize card size
        CardView.WIDTH = Math.round((square ? 0.1875f * 0.7f : (settings.isLandscape() ? 0.1875f * 0.5f : 0.1875f)) * width);
        CardView.HEIGHT = Math.round(CardView.WIDTH / (square ? 0.685f : (settings.isLandscape() ? 0.714f : 0.64f)));

        PADDING = CardView.WIDTH / 10;
        Controller.OFFSET = PADDING * 2;

        areas.setup(width, height, settings.isLandscape());

        layoutTalonCards();
        //layoutTrick();
        layoutAICards(settings, 1);
        layoutAICards(settings, 2);
        layoutHumanCards(settings);
        layoutMisereDesk(g, settings.isLandscape());
        layoutDropCards(g);

        views.trick.layout(areas.trick);
        views.firstHand.layout();
        views.sandClock.layout();
        views.turn.layout();
      }

      for (Player p : g.players())
      {
        views.bids[p.id()].layout(areas.bid[p.id()]);
        views.trickCounter[p.id()].layout(areas.trickCounter[p.id()]);
      }

      layoutInputControls(settings);

      views.undoDrop.layout();
      views.offer.layout();

      views.watermark.show(areas.watermark, g);

      for (Mark mark : views.mark)
        mark.layout();

      views.note.layout();
    }
  }

  private void hideTrick(Game g, boolean talon)
  {
    views.trick.hide();

    if (talon)
      views.talon.get(g.rounds).setVisibility(View.INVISIBLE);
  }

  private final class Views
  {
    private final class Input
    {
      //public final FrameLayout layout = (FrameLayout)findViewById(R.id.input);
      private final View frame0 = findViewById(R.id.input_frame0);
      private final LinearLayout frame1 = (LinearLayout)findViewById(R.id.input_frame1);
      private final View frame2 = findViewById(R.id.input_frame2);
      private final BidButton bid = (BidButton)findViewById(R.id.bid);
      private final TextButton pass = (TextButton)findViewById(R.id.pass);
      private final TextButton misere = (TextButton)findViewById(R.id.misere);
      private final TextButton whistFull = (TextButton)findViewById(R.id.whist_full);
      private final TextButton whistHalf = (TextButton)findViewById(R.id.whist_half);
      private final TextButton whistPass = (TextButton)findViewById(R.id.whist_pass);
      private final TextButton stand2 = (TextButton)findViewById(R.id.stand2);
      private final TextButton layDown2 = (TextButton)findViewById(R.id.lay_down2);
      private final TextButton stand = (TextButton)findViewById(R.id.stand);
      private final TextButton layDown = (TextButton)findViewById(R.id.lay_down);
      private final TextButton drop = (TextButton)findViewById(R.id.drop);
      private final TextButton without3 = (TextButton)findViewById(R.id.without3);
      private final TextButton agree10 = (TextButton)findViewById(R.id.tricking10_agree);
      private final TextButton play10 = (TextButton)findViewById(R.id.tricking10_play);
      private final BidButton declare = (BidButton)findViewById(R.id.declare);
      private final RankButton declareRankUp = (RankButton)findViewById(R.id.declare_rank_up);
      private final RankButton declareRankDown = (RankButton)findViewById(R.id.declare_rank_down);
      private final SuitButton declareSuitUp = (SuitButton)findViewById(R.id.declare_suit_up);
      private final SuitButton declareSuitDown = (SuitButton)findViewById(R.id.declare_suit_down);
      private final TextButton miserePlay = (TextButton)findViewById(R.id.misere_play);

      private final View[] elements = {
          pass, bid, misere, whistFull, whistHalf, whistPass, declare, declareRankUp, declareRankDown, declareSuitUp,
          declareSuitDown, stand, layDown, stand2, layDown2, drop, without3, agree10, play10, miserePlay
      };

      private Input()
      {
        // setup option buttons
        setup(bid, R.id.bid);
        setup(pass, R.id.pass);
        setup(misere, R.id.misere);
        setup(without3, R.id.without3);
        setup(drop, R.id.drop);
        setup(whistFull, R.id.whist_full);
        setup(whistHalf, R.id.whist_half);
        setup(whistPass, R.id.whist_pass);
        setup(miserePlay, R.id.misere_play);
        setup(agree10, R.id.tricking10_agree);
        setup(play10, R.id.tricking10_play);
        setup(stand, R.id.stand);
        setup(stand2, R.id.stand2);
        setup(layDown, R.id.lay_down);
        setup(layDown2, R.id.lay_down2);
        setup(declare, R.id.declare);

        // setup suit buttons
        setup(declareSuitUp);
        setup(declareSuitDown);

        // setup rank buttons
        setup(declareRankUp);
        setup(declareRankDown);
      }

      private void setup(TwoLayerButton tlb, final int id)
      {
        tlb.setOnClickListener(new OnClickListener() {
          @Override
          public void onClick(View view)
          {
            controller.onInputButtonClicked(view, id);
          }
        });
      }

      private void setup(final BidShifter bs)
      {
        ((TwoLayerButton)bs).setOnClickListener(new OnClickListener()
        {
          @Override
          public void onClick(View view)
          {
            // check view's enable state first as onClick event might occur more than once if user presses on button too fast
            if (view.isEnabled())
            {
              Game g = game();

              bs.shift(g.options.bid);
              update4WayDeclareControl(g, g.options.bid);
            }
          }
        });
      }

      private void show4WayDeclareControl(Game g, Bid bid)
      {
        views.input.frame1.setVisibility(VISIBLE);
        views.input.declare.setVisibility(VISIBLE);
        views.input.declareRankUp.setVisibility(VISIBLE);
        views.input.declareRankDown.setVisibility(VISIBLE);
        views.input.declareSuitUp.setVisibility(VISIBLE);
        views.input.declareSuitDown.setVisibility(VISIBLE);

        update4WayDeclareControl(g, bid);
      }

      private void update4WayDeclareControl(Game g, Bid bid)
      {
        declare.show(bid);
        declareRankUp.set(g, bid);
        declareRankDown.set(g, bid);
        declareSuitUp.show(g, bid);
        declareSuitDown.show(g, bid);

        if (g.tutorial())
        {
          Bid target = g.scenario().contract(g.rounds, g.human().id());

          declare.unmark();
          declareRankUp.unmark();
          declareRankDown.unmark();
          declareSuitUp.unmark();
          declareSuitDown.unmark();

          if (!bid.equals(target))
          {
            if (bid.tricks < target.tricks)
              declareRankUp.mark();
            else
              if (bid.tricks > target.tricks)
                declareRankDown.mark();

            if (bid.trump.ordinal() < target.trump.ordinal())
              declareSuitUp.mark();
            else
              if (bid.trump.ordinal() > target.trump.ordinal())
                declareSuitDown.mark();
          }
          else
            declare.mark();
        }
      }
    }

    // input buttons
    private Input input;

    // card views
    private final List<List<CardView>> hands = new ArrayList<List<CardView>>();
    private final List<CardView> talon = new ArrayList<CardView>(Game.TALON_SIZE);
    private final List<CardView> drop = new ArrayList<CardView>(Game.TALON_SIZE);

    // elements
    private final Watermark watermark;
    private final FirstHand firstHand;
    private final SandClock sandClock;
    private final Turn turn;
    private final UndoDrop undoDrop;
    private final Offer offer;
    private final TrickElement trick;
    private final MisereDesk misere;
    private final TrickCounter[] trickCounter = new TrickCounter[Game.PLAYERS];
    private final BidElement[] bids = new BidElement[Game.PLAYERS];
    private final Mark[] mark = new Mark[2];
    private final TutorialNote note;

    Views(Context context)
    {
      watermark = new Watermark(context, TableView.this);
      firstHand = new FirstHand(context, TableView.this);
      sandClock = new SandClock(context, TableView.this);
      turn = new Turn(context, TableView.this);
      undoDrop = new UndoDrop(context, TableView.this);
      offer = new Offer(context, TableView.this);

      undoDrop.setOnClickListener(new OnClickListener()
      {
        @Override
        public void onClick(View view)
        {
          if (view.isShown())
            listener.onUndoDropButtonClicked(game());
        }
      });

      offer.setOnClickListener(new OnClickListener()
      {
        public void onClick(View view)
        {
          if (view.isShown())
            controller.onOfferButtonClicked(view);
        }
      });

      // create talon card views
      create(context, talon, Game.TALON_SIZE, true);
      // create drop card views
      create(context, drop, Game.TALON_SIZE, false);

      // trick view
      trick = new TrickElement(context, TableView.this);

      // create callouts and trick counters
      for (int i = 0; i < Game.PLAYERS; ++i)
      {
        // create bid views
        bids[i] = (i == 2 ? new BidRight(context, TableView.this) : new BidLeft(context, TableView.this));

        // create trick counter views
        trickCounter[i] = new TrickCounter(context, TableView.this);
        trickCounter[i].show(true);
      }

      // create card views for players (hands)
      for (int i = 0; i < Game.PLAYERS; ++i)
      {
        List<CardView> hand = new ArrayList<CardView>();

        // create card views for player "i"
        create(context, hand, Game.HAND_SIZE + Game.TALON_SIZE, false);
        hands.add(hand);
      }

      Collections.reverse(hands);

      // create misere desk view (after the cards are created to ensure it's on top of them)
      misere = new MisereDesk(context, TableView.this);

      // create tutorial mark images to be shown over card views
      for (int i = 0; i < mark.length; ++i)
        mark[i] = new Mark(context, TableView.this);

      // create tutorial note
      note = new TutorialNote(context, TableView.this, new OnClickListener()
      {
        @Override
        public void onClick(View view)
        {
          Game g = game();

          // trigger onTouch event for TableView
          if (g.options.flag() == ferp.core.game.Input.Options.WAIT_FOR_TOUCH || g.mode().mover(g, settings(), g.player.current) != g.human())
            dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0));
        }
      });
    }

    private void initialize() {input = new Input();}

    // create a number of CardView objects designated by "count", adding them to the supplied list and to "all"
    private void create(Context context, List<CardView> list, int count, boolean talon)
    {
      for (int i = 0; i < count; ++i)
      {
        CardView cv = new CardView(context, TableView.this);

        // populate the supplied list
        list.add(cv);
        // disable
        cv.setEnabled(false);

        // add the card view to the list of this view groups' children
        if (!talon)
          addView(cv);
      }

      if (talon)
      {
        ListIterator<CardView> iter = list.listIterator(list.size());

        while (iter.hasPrevious())
          addView(iter.previous());
      }
    }

    // find a card view backed by a provided card
    private CardView find(int id, Card c)
    {
      List<CardView> hand = hands.get(id);

      for (CardView cv : hand)
        if (cv.card() == c)
          return cv;

      return null;
    }
  }

  private static final class Areas
  {
    final Rect[] bid = rect(3);
    final Rect[] firstHand = rect(3);
    final Rect[] trickCounter = rect(3);
    final Rect[] turn = rect(3);
    final Rect offer = new Rect();
    final Rect[] clock = rect(3);
    final Rect talon = new Rect();
    final Rect trick = new Rect();
    final Rect sink = new Rect();
    final Rect[] hand = rect(3);
    final Rect[] undoDrop = rect(2);
    final Rect watermark = new Rect();
    final Rect[] drop = rect(3);
    final Rect[] misere = rect(3);
    final Rect stand = new Rect();
    final Rect[] mark = rect(2);
    final Rect[] note = rect(3);

    // paint
    final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    
    void setup(int width, int height, boolean landscape)
    {
      // layout area paint
      //paint.setColor(0xFF006600);
      paint.setColor(Color.RED);
      paint.setStyle(Paint.Style.STROKE);

      // trick counter size
      int tcs = Math.round(landscape ? width / 15.f : CardView.WIDTH / 1.4f);

      // layout Android callout areas
      int cow = CardView.WIDTH;
      int coh = tcs;

      if (landscape)
      {
        set(bid[1], PADDING + CardView.WIDTH + PADDING, PADDING, cow, coh);
        set(bid[2], width - PADDING - CardView.WIDTH - PADDING - cow, PADDING, cow, coh);
      }
      else
      {
        set(bid[1], PADDING, PADDING, cow, coh);
        set(bid[2], width - PADDING - CardView.WIDTH, PADDING, cow, coh);
      }

      // layout hand areas
      int aht;                                                          // android hand top
      int ahh;                                                          // android hand height
      int hhx;                                                          // human hand x
      int hht;                                                          // human hand top
      int hhw;                                                          // human hand width

      if (landscape)
      {
        aht = PADDING;
        ahh = height - PADDING - CardView.HEIGHT / 2;
        hhx = CardView.WIDTH + 2 * PADDING;
        hht = height - CardView.HEIGHT - PADDING;
        hhw = width - 2 * CardView.WIDTH - PADDING * 4;
      }
      else
      {
        aht = bid[1].height() + PADDING * 2;
        ahh = height - aht - 2 * PADDING - CardView.HEIGHT;
        hhx = PADDING;
        hht = aht + ahh + PADDING;
        hhw = width - PADDING * 2;
      }

      set(hand[1], PADDING, aht, CardView.WIDTH, ahh);
      set(hand[2], width - PADDING - CardView.WIDTH, hand[1].top, CardView.WIDTH, ahh);
      set(hand[0], hhx, hht, hhw, CardView.HEIGHT);

      // layout trick counter areas
      set(trickCounter[0], (width - tcs) / 2, height - 2 * PADDING - CardView.HEIGHT - tcs, tcs, tcs);
      if (landscape)
      {
        set(trickCounter[1], hand[1].right + PADDING, bid[1].bottom + PADDING, tcs, tcs);
        set(trickCounter[2], hand[2].left - PADDING - tcs, bid[2].bottom + PADDING, tcs, tcs);
      }
      else
      {
        set(trickCounter[1], hand[1].right + PADDING, hand[1].top, tcs, tcs);
        set(trickCounter[2], hand[2].left - PADDING - tcs, hand[2].top, tcs, tcs);
      }

      // layout human callout area (same height as the trick counter)
      set(bid[0], GUI.center(cow, hand[1].right, trickCounter[0].left), trickCounter[0].top, cow, tcs);

      // layout first hand areas
      int fhw = Math.min(width, height) / 10;
      int fhh = tcs;
      set(firstHand[0], trickCounter[0].right + (hand[2].left - trickCounter[0].right - fhw) / 2, trickCounter[0].top, fhw, fhh);
      if (landscape)
      {
        set(firstHand[1], trickCounter[1].left + (tcs - fhw) / 2, trickCounter[1].bottom + PADDING, fhw, fhh);
        set(firstHand[2], trickCounter[2].left + (tcs - fhw) / 2, trickCounter[2].bottom + PADDING, fhw, fhh);
      }
      else
      {
        set(firstHand[1], trickCounter[1].left + (tcs - fhw) / 2, bid[1].top, fhw, fhh);
        set(firstHand[2], trickCounter[2].left + (tcs - fhw) / 2, firstHand[1].top, fhw, fhh);
      }

      // layout turn areas
      set(turn[0], GUI.center(tcs, trickCounter[0].right, hand[2].left), trickCounter[0].top, tcs, tcs);
      set(turn[1], trickCounter[1].left, trickCounter[1].bottom + PADDING, tcs, tcs);
      set(turn[2], trickCounter[2].left, turn[1].top, tcs, tcs);

      // layout offer area
      set(offer, GUI.center(tcs, trickCounter[0].right, hand[2].left), turn[0].top - tcs - PADDING, tcs, tcs);

      // layout clock areas
      int cw = Math.round(Math.min(width, height) / 11.5f);
      int ch = tcs;
      set(clock[0], trickCounter[0].right + (hand[2].left - trickCounter[0].right - cw) / 2, trickCounter[0].top, cw, ch);
      set(clock[1], trickCounter[1].left + (tcs - cw) / 2, trickCounter[1].bottom + PADDING, cw, ch);
      set(clock[2], trickCounter[2].left + (tcs - cw) / 2, clock[1].top, cw, ch);

      // layout talon area
      int tw = Math.round(Math.min(trickCounter[2].left - trickCounter[1].right - 2 * PADDING, 1.5f * CardView.WIDTH));
      set(talon, (width - tw) / 2, PADDING, tw, CardView.HEIGHT);

      // layout sink area
      if (landscape)
        sink.set(trickCounter[1].centerX(), trickCounter[1].centerY(), trickCounter[2].centerX(), trickCounter[0].centerY());
      else
        sink.set(trickCounter[1].centerX(), Math.max(trickCounter[1].bottom, talon.bottom) + PADDING, trickCounter[2].centerX(), trickCounter[0].top - PADDING);

      // layout trick area - make it 10% wider than talon
      int th = Math.round(1.3f * CardView.HEIGHT);
      tw = Math.round(tw * 1.1f);
      set(trick, GUI.center(tw, talon.left, talon.right), GUI.center(th, sink.top, sink.bottom), tw, th);

      // undo drop areas
      int udw = trickCounter[0].width(), udh = udw;
      set(undoDrop[0], GUI.center(udw, hand[1].right, trickCounter[0].left), trickCounter[0].top, udw, udh);
      set(undoDrop[1], undoDrop[0].left, bid[0].top - PADDING - undoDrop[0].height(), udw, udh);

      int ww = hand[2].left - hand[1].right, wh = ww / 3;
      set(watermark, hand[1].right, GUI.center(wh, trickCounter[1].bottom, offer.top), ww, wh);

      // initial position's drop area
      set(drop[0], talon.left, trickCounter[0].top - CardView.HEIGHT - PADDING, tw, CardView.HEIGHT);
      set(drop[1], hand[1].right + PADDING, GUI.center(CardView.HEIGHT, hand[1].top, hand[1].bottom), tw, CardView.HEIGHT);
      set(drop[2], hand[2].left - drop[0].width() - PADDING, drop[1].top, tw, CardView.HEIGHT);

      // standing/lay down area
      if (!landscape)
      {
        int sw = trickCounter[2].right - trickCounter[1].left;
        int sh = trickCounter[0].top - trick.top - CardView.HEIGHT;

        set(stand, trickCounter[1].left, trick.top + CardView.HEIGHT, sw, sh);
      }

      int nl, nw;

      if (landscape)
      {
        nl = Math.max(trickCounter[1].right, bid[1].right) + PADDING;
        nw = Math.min(trickCounter[2].left, bid[2].left) - PADDING - nl;
      }
      else
      {
        nl = hand[1].left + CardView.WIDTH / 2;
        nw = hand[2].right - CardView.WIDTH / 2 - nl;
      }

      int nt0 = PADDING / 2, nh0 = Math.max(trickCounter[1].bottom, talon.bottom);
      int nt2 = hand[0].top - PADDING / 2, nh2 = CardView.HEIGHT + PADDING;
      int nt1 = GUI.center(nh2, nt0 + nh0, nt2), nh1 = nh2;

      set(note[Scenario.Note.Position.TOP.ordinal()], nl, nt0, nw, nh0);
      set(note[Scenario.Note.Position.CENTER.ordinal()], nl, nt1, nw, nh1);
      set(note[Scenario.Note.Position.BOTTOM.ordinal()], nl, nt2, nw, nh2);
    }
    
    void set(Rect r, int x, int y, int width, int height)
    {
      r.set(x, y, x + width, y + height);
    }

    Rect[] rect(int n)
    {
      Rect[] ra = new Rect[n];

      for (int i = 0; i < ra.length; ++i)
        ra[i] = new Rect();

      return ra;
    }
    
    void draw(Canvas c)
    {
/*
      c.drawRect(talon, paint);
      c.drawRect(trick, paint);
      c.drawRect(sink, paint);
      c.drawRect(hand[0], paint);
      c.drawRect(hand[1], paint);
      c.drawRect(hand[2], paint);

      for (Rect r : trickCounter)
        c.drawRect(r, paint);

      for (Rect r : callout)
        c.drawRect(r, paint);

      for (Rect r : glove)
        c.drawRect(r, paint);

      for (Rect r : clock)
        c.drawRect(r, paint);
*/
    }
  }

  // profile manager
  private ProfileManager pm;

  // common padding
  private static int PADDING;
  // event listener
  private Listener listener;
  // square screen?
  private boolean square;
  // areas
  private final Areas areas = new Areas();
  // views object (aggregates trick, talon and player views)
  private Views views;
  // currently moving card
  // currently moving card
  private MovingCard mc;

  // controllers
  private Controller default_;
  private DropController drop;
  private CardDragController drag;
  private CardDoubleTapController doubleTap;
  private Controller controller;

  private int countVisibleCards(List<CardView> list)
  {
    int visible = 0;

    for (CardView cv : list)
      if (cv.getVisibility() == VISIBLE)
        visible++;

    return visible;
  }

  private int countSuitGaps(Settings settings, List<CardView> list)
  {
    // only if suit spacing configured
    if (!settings.suitSpacing)
      return 0;

    int[] counts = {0, 0, 0, 0};

    // increment appropriate suit count only if the card is face up
    for (CardView cv : list)
      if (cv.getVisibility() == VISIBLE && cv.isFaceUp())
        ++counts[cv.card().suit.ordinal()];

    int result = 0;

    // summarize suits
    for (int n : counts)
      if (n > 0)
        ++result;

    // no cards face up - no gaps
    return result == 0 ? 0 : result - 1;
  }

  private void layoutHumanCards(Settings settings)
  {
    Rect area = areas.hand[0];
    List<CardView> list = views.hands.get(0);
    int n = countVisibleCards(list);
    int gaps = countSuitGaps(settings, list);
    int w = area.width();
    int spacing = w / 20;
    // next card offset
    double nco = ((double)w - CardView.WIDTH - spacing * gaps) / (n > Game.HAND_SIZE ? Game.HAND_SIZE + 1 : Game.HAND_SIZE - 1);
    // center the whole hand
    double left = area.left + (n > 0 ? (w - CardView.WIDTH - nco * (n - 1) - spacing * gaps) / 2 : 0);
    // previous visible card (to place spacing between suits)
    CardView previous = null;

    for (CardView cv : list)
      if (cv.getVisibility() != GONE)
      {
        // only if there is at least one gap
        if (gaps > 0 && previous != null && cv.card().suit != previous.card().suit)
          left += spacing;

        controller.layout(0, cv, (int)left, area.top);

        left += nco;
        previous = cv;
      }
  }

  private void layoutAICards(Settings settings, int id)
  {
    List<CardView> list = views.hands.get(id);
    Rect area = areas.hand[id];
    int n = countVisibleCards(list);
    int h = area.height();
    int gaps = countSuitGaps(settings, list);
    int spacing = h / 30;
    // next card offset
    double nco = ((double)h - CardView.HEIGHT - spacing * gaps) / (n > Game.HAND_SIZE ? Game.HAND_SIZE + 1 : Game.HAND_SIZE - 1);
    // center the whole hand
    double top = area.top + (n > 0 ? (h - CardView.HEIGHT - nco * (n - 1) - spacing * gaps) / 2 : 0);
    // previous visible card (to place spacing between suits)
    CardView previous = null;

    for (CardView cv : list)
      if (cv.getVisibility() != GONE)
      {
        // only if there is at least one  gap
        if (gaps > 0 && previous != null && cv.card().suit != previous.card().suit)
          top += spacing;

        controller.layout(id, cv, area.left, (int)top);

        top += nco;
        previous = cv;
      }
  }

  private void layoutTalonCards()
  {
    Rect area = areas.talon;

    views.talon.get(0).layout(area.left, area.top);
    views.talon.get(1).layout(area.right - CardView.WIDTH, area.top);
  }

  private void layoutDropCards(Game g)
  {
    if (g.player.declarer != Game.NO_ID)
    {
      Rect area = areas.drop[g.player.declarer];
      CardView cv0 = views.drop.get(0);
      CardView cv1 = views.drop.get(1);

      cv0.measure(MeasureSpec.EXACTLY | CardView.WIDTH, MeasureSpec.EXACTLY | area.height());
      cv0.layout(area.left, area.top);

      cv1.measure(MeasureSpec.EXACTLY | CardView.WIDTH, MeasureSpec.EXACTLY | area.height());
      cv1.layout(area.right - CardView.WIDTH, area.top);
    }
  }

  private void layoutMisereDesk(Game g, boolean landscape)
  {
    if (g.mode() == Mode.misere && g.declarer() != g.human())
      views.misere.layout(areas.misere[g.player.declarer], areas.bid[0], areas.trick, areas.turn[0], areas.hand[2], areas.trickCounter[0], landscape, PADDING, g.player.declarer);
  }

  private void layoutInputControls(Settings settings)
  {
    if (settings.orientation == Settings.Orientation.PORTRAIT)
    {
      int t0 = Math.max(areas.trickCounter[1].bottom, areas.talon.bottom),
          l = areas.trickCounter[1].left,
          w = areas.trickCounter[2].right - l,
          h0 = areas.trickCounter[0].top - t0,
          t1 = areas.trickCounter[1].bottom,
          h1 = areas.trickCounter[0].top - t1;

      //views.input.layout.measure(MeasureSpec.EXACTLY | w, MeasureSpec.EXACTLY | (areas.trickCounter[0].top - areas.trickCounter[1].bottom));

      views.input.frame0.measure(MeasureSpec.EXACTLY | w, MeasureSpec.EXACTLY | h0);
      views.input.frame0.layout(l, t0, l + w, t0 + h0);

      views.input.frame1.measure(MeasureSpec.EXACTLY | w, MeasureSpec.EXACTLY | h1);
      views.input.frame1.layout(l, t1, l + w, t1 + h1);

      //views.input.layout.layout(l, t, l + w, t + views.input.layout.getMeasuredHeight());

      views.input.frame2.measure(MeasureSpec.EXACTLY | areas.stand.width(), MeasureSpec.EXACTLY | areas.stand.height());
      //views.input.frame2.layout(areas.bo.left, areas.bo.top, areas.bo.right, areas.bo.bottom);
      views.input.frame2.layout(areas.stand.left, areas.stand.top, areas.stand.right, areas.stand.bottom);
    }
    else
    {
      int t0 = Math.max(areas.trickCounter[1].bottom, areas.talon.bottom),
          t1 = areas.trickCounter[1].top,
          l = areas.trickCounter[1].right,
          r = areas.trickCounter[2].left,
          w = r - l,
          b = areas.trickCounter[0].top,
          h0 = b - t0,
          h1 = b - t1;

      views.input.frame0.measure(MeasureSpec.EXACTLY | w, MeasureSpec.EXACTLY | h0);
      views.input.frame0.layout(l, t0, r, b);
      views.input.frame1.measure(MeasureSpec.EXACTLY | w, MeasureSpec.EXACTLY | h1);
      views.input.frame1.layout(l, t1, r, b);
    }
  }

  private void showMark(TwoLayerButton tlb) {tlb.mark();}

  private void showDropCards(Game g)
  {
    for (int i = 0; i < Game.TALON_SIZE; ++i)
      views.drop.get(i).set(Card.card(g.drop()[i]), true);
  }
}
